Skip to content

S01-08 JavaSE-面向对象-基础

[TOC]

概述

入门案例:养猫猫问题

养猫猫问题:张老太养了两只猫猫:一只名字叫小白,今年 3 岁,白色;另一只叫小花,今年 100 岁,花色。编写程序,当用户输入小猫的名字时,显示该猫的名字、年龄、颜色;若输入错误,显示“张老太没有这只猫猫”。

现有技术解决

方式 1:单独定义变量
java
// 第1只猫信息
String cat1Name = "小白";
int cat1Age = 3;
String cat1Color = "白色";
// 第2只猫信息
String cat2Name = "小花";
int cat2Age = 100;
String cat2Color = "花色";

缺点:不利于数据管理,变量过多易混乱。

方式 2:使用数组
java
// 第1只猫信息
String[] cat1 = {"小白", "3", "白色"};
// 第2只猫信息
String[] cat2 = {"小花", "100", "花色"};

缺点

  1. 数据类型不明确(年龄是字符串类型)
  2. 只能通过下标获取信息,对应关系不清晰
  3. 不能体现猫的行为(如叫、跑)
现有技术缺点

现有技术不利于数据管理、效率低,引出面向对象编程(OOP)

面向对象解决

实现思路:定义Cat类(自定义数据类型),包含属性(名字、年龄、颜色)和行为,通过创建对象管理每只猫的信息。

java
public class Object01 {
    // 编写一个main方法
    public static void main(String[] args) {
        // 2. 实例化 Cat 类
        // 实例化第一只猫(创建对象),并赋值给 cat1 变量
        Cat cat1 = new Cat();
        cat1.name = "小白";
        cat1.age = 3;
        cat1.color = "白色";
        cat1.weight = 10;
        // 实例化第二只猫,并赋值给 cat1 变量
        Cat cat2 = new Cat();
        cat2.name = "小花";
        cat2.age = 100;
        cat2.color = "花色";
        cat2.weight = 20;

        // 3. 访问对象属性
        System.out.println("第1只猫信息:" + cat1.name + " " + cat1.age + " " + cat1.color + " " + cat1.weight);
        System.out.println("第2只猫信息:" + cat2.name + " " + cat2.age + " " + cat2.color + " " + cat2.weight);
    }
}

// 1. 定义猫类 Cat(自定义数据类型)
class Cat {
    // 属性(成员变量)
    String name; // 名字
    int age; // 年龄
    String color; // 颜色
    double weight; // 体重
    // 行为(后续补充)
}

面向对象编程

面向对象编程(OOP,Object-Oriented Programming):是一种以对象为核心的编程范式,它将现实世界中的事物抽象为程序中的对象,通过封装对象的属性和行为、建立对象间的继承与多态关系,来组织和构建程序。

Java 是纯粹的面向对象语言(除基本数据类型外,所有事物都可抽象为对象,且基本类型也有对应的包装类),OOP 是 Java 的核心思想,其核心在于类与对象,并通过封装、继承、多态三大特性实现代码的高复用、易维护和可扩展。

类与对象

类(Class)对象的 “模板” 或 “蓝图”,是对一类具有相同 属性(状态)方法(行为) 的事物的抽象描述。

  • 属性:也叫成员变量 / 字段(Field),表示对象的状态(比如人的姓名、年龄)。
  • 方法:也叫成员方法(Method),表示对象的行为(比如人的吃饭、跑步)。

对象(Object)类的 “实例”,是类的具体实现,是内存中实际存在的实体。通过new关键字可以创建类的对象,每个对象都拥有类定义的属性和方法的独立副本。

类与对象的关系图

猫类(Cat)→ 提取共性:属性(name, age, color)、行为(run, cry...)

对象1(小白):name="小白",age=3,color="白色"

对象2(小花):name="小花",age=100,color="花色"

对象内存图

对象在内容中的存在形式如图:

image-20251217165829493

基础

属性

属性(成员变量,字段,Field):表示对象的状态(比如人的姓名、年龄)。

定义语法

访问修饰符:public、protected、默认、private,用于控制属性的访问范围。

java
访问修饰符 属性类型 属性名;

// 示例
class Car {
  protected String name;
}

注意事项

  1. 属性的类型:属性可以是基本数据类型(int、double)或引用类型(对象、数组)。

    java
    class Car {
        // 基本类型
        double price;
    
        // 引用类型
        String name;
        String color;
        String[] masters;
    }
  2. 属性的默认值:未赋值的属性有默认值,规则同数组

    • 基本类型:
      • int 0short 0byte 0long 0
      • float 0.0double 0.0
      • char \u0000
      • boolean false
    • 引用类型(如 String):null
    java
    // 属性的默认值
    public class PropertiesDetail {
        public static void main(String[] args) {
            Person p1 = new Person();
            System.out.println(
            	"age=" + p1.age + " name=" + p1.name + " sal=" + p1.sal + " isPass=" + p1.isPass
            ); // age=0,name=null,sal=0.0,isPass=false
        }
    }
    
    class Person {
        int age; // 默认值:0
        String name; // 默认值:null
        double sal; // 默认值:0.0
        boolean isPass; // 默认值:false
    }

对象创建

对象创建有以下 2 种方式:

  1. 方式 1:先声明再创建

    java
    Cat cat; // 声明对象(栈中存储引用)
    cat = new Cat(); // 创建对象(堆中分配空间)
  2. 方式 2:直接创建

    java
    Cat cat = new Cat();

访问属性

语法

java
对象名.属性名;

示例:属性赋值/取值

java
cat.name = "小白"; // 赋值
System.out.println(cat.age); // 取值

内存流程图:对象创建

示例代码

java
Person p1 = new Person();
p1.age = 10;
p1.name = "小明";
Person p2 = p1; // p2指向p1引用的对象
System.out.println(p2.age); // 输出10(p1和p2操作同一个对象)

创建对象的流程

Person p = new Person();为例:

  1. 加载Person类信息(属性和方法,仅加载一次)。
  2. 在堆中分配空间,进行默认初始化(属性赋默认值)。
  3. 将堆中对象地址赋给栈中的引用变量p
  4. 进行指定初始化(如p.name = "jack")。

内存流程图

  1. Person p1 = new Person();

    image-20251217175222183

  2. p1.age = 10; p1.name = "小明";

    image-20251217174623501

  3. Person p2 = p1;

    image-20251217174706845

  4. System.out.println(p2.age);

    image-20251217174856697

练习:对象内存流程图

练习:画出以下代码的内存流程图

java
Person a = new Person();
a.age = 10;
a.name = "小明";

Person b;
b = a;
System.out.println(b.name); // "小明"

b.age = 200;
b = null;
System.out.println(a.age); // 200
System.out.println(b.age); // 抛出异常:java.lang.NullPointerException

image-20251217180753920

成员方法

方法(Method,成员方法):表示对象的行为(比如人的吃饭、跑步)。

快速入门

Person 类添加方法:

  1. speak():输出“我是一个好人”
  2. cal01():计算 1+2+...+1000 的结果
  3. cal02(int n):计算 1+2+...+n 的结果
  4. getSum(int num1, int num2):计算两个数的和
java
public class Method01 {
    public static void main(String[] args) {
        // 创建Person对象
        Person p1 = new Person();

        // 调用方法
        p1.speak(); // 输出"我是一个好人"
        p1.cal01(); // 输出1+...+1000的结果
        p1.cal02(5); // 输出1+...+5的结果
        p1.cal02(10); // 输出1+...+10的结果
        int sum = p1.getSum(10, 20); // 接收方法返回值
        System.out.println("两数之和:" + sum);
    }
}

class Person {
    String name;
    int age;

    // 1. speak方法:无参数、无返回值
    public void speak() {
        System.out.println("我是一个好人");
    }

    // 2. cal01方法:无参数、无返回值
    public void cal01() {
        int res = 0;
        for (int i = 1; i <= 1000; i++) {
            res += i;
        }
        System.out.println("1+...+1000的结果:" + res);
    }

    // 3. cal02方法:有参数、无返回值
    public void cal02( int n ) { // 形参
        int res = 0;
        for (int i = 1; i <= n; i++) {
            res += i;
        }
        System.out.println("1+...+" + n + "的结果:" + res);
    }

    // 4. getSum方法:有参数、有返回值
    public int getSum(int num1, int num2) { // 形参列表
        return num1 + num2;  // 返回值
    }
}

语法结构

语法格式

一个完整的 Java 成员方法由多个组成部分构成,语法格式如下:

java
[修饰符列表] [返回值类型] 方法名([参数列表]) [throws 异常类型列表] {
    // 方法体:具体的功能逻辑代码
    [return 返回值]
}

下面对每个组成部分进行详细解析:

修饰符列表

修饰符分为访问修饰符非访问修饰符,可组合使用(需遵循语法规则,比如abstractstatic不能同时修饰一个方法)。

访问修饰符
修饰符可见范围说明
public所有类(跨包、跨类)最宽松的访问权限
protected本类、同包子类、不同包子类继承相关的访问权限
default本类、同包类(无修饰符)默认访问权限
private仅本类最严格的访问权限
非访问修饰符
修饰符作用
static定义类方法(静态方法),属于类而非对象
final方法不能被子类重写(Override)
abstract定义抽象方法,无方法体,仅声明方法签名(只能在抽象类/接口中)
native本地方法,方法体由非 Java 代码(如 C/C++)实现,仅声明签名
synchronized同步方法,保证多线程环境下的原子性(避免线程安全问题)
strictfp严格浮点计算,保证不同平台下浮点运算结果一致

返回值

核心特性

方法执行完成后返回给调用者的结果类型,分为两种情况:

  1. 有返回值

    1. 可以是 Java 的基本数据类型(int/double等)、引用数据类型(String/Object/自定义类等)。

    2. 此时方法体中必须通过return语句返回对应类型(或类型兼容)的值,且return后不能有多余代码。

      image-20251229143340624

      image-20251229143420893

      image-20251229143608526

  2. 无返回值:用void表示,方法体中可以省略return语句(或写return;表示结束方法)。

    java
    // 无返回值:仅打印信息
    public void printInfo(String msg) { 
        System.out.println(msg);
        // 可省略return,也可写return;
    }
  3. 返回值数量:一个方法最多有一个返回值,如需返回多个结果,可使用数组

    image-20251229142527474

    image-20251229142829554

方法名

遵循 Java 的命名规范

  • 采用小驼峰命名法(首字母小写,后续单词首字母大写,如eatFoodcalculateSum)。
  • 见名知意,准确描述方法的功能(如getAge表示获取年龄,setName表示设置姓名)。
  • 不能与关键字重名,且不能包含空格特殊符号(除_$)。

参数列表

基本格式

基本格式:多个参数用逗号分隔,每个参数的格式为:

java
// 基本格式
参数类型 参数名

// 示例
int a, String b

核心特性

  1. 参数个数:一个方法可以有 0 个参数,也可以有多个参数,参数中间用 , 逗号间隔。

  2. 参数类型:参数可以为任意类型(包括基本类型和引用类型)。

  3. 形参/实参

    • 实参调用方法时传递给形参的具体值。
    • 形参定义方法时的参数称为形参,
    • 要求**实参的类型、个数、顺序必须与形参兼容**(自动类型转换除外,如int可赋值给double)。
    java
    class AA {
        // 1. 方法定义(形参)
        public int[] getSumAndSub(int n1, int n2) {  // 形参
            int[] resArr = new int[2];
            resArr[0] = n1 + n2;
            resArr[1] = n1 - n2;
            return resArr;
        }
    }
    
    public class MethodDetail {
        public static void main(String[] args) {
            // 2. 方法调用(实参)
            AA a = new AA();
    
            // ✅ 兼容:int -> int
            a.getSumAndSub(10, 4);  // 实参
    
            // ✅ 兼容:byte -> int
            byte b1 = 1, b2 = 2;
            a.getSumAndSub(b1, b2);  // 实参
    
            // ❌ 不兼容:double -> int
            a.getSumAndSub(1.1, 1.2);  // 实参
        }
    }
可变参数(不定长参数)

JDK 5 引入的特性,允许方法接收任意个数的同类型参数,语法为:参数类型... 参数名(本质是数组)。

  • 可变参数必须放在参数列表的最后一位
  • 一个方法只能有一个可变参数。

示例

java
// 可变参数:计算多个int的和
public int sum(int... nums) { 
    int total = 0;
    for (int num : nums) { // 可变参数可当作数组遍历
        total += num;
    }
    return total;
}

// 调用:可传入任意个数的int
sum(1, 2); // 结果3
sum(1, 2, 3, 4); // 结果10

异常声明(throws)

声明方法可能抛出的受检异常(Checked Exception),告诉调用者需要处理这些异常(要么try-catch捕获,要么继续throws)。

示例

java
// 声明方法可能抛出IOException
public void readFile(String path) throws IOException { 
    // 读取文件的逻辑,可能抛出IOException
}

方法体

方法的核心逻辑代码块,包含变量定义、语句执行、流程控制(分支、循环)、调用其他方法等。

核心特性

  1. 访问成员/局部变量

    • 方法体中可以访问类的成员变量(实例变量/静态变量)和局部变量(方法内定义的变量)。

    • 局部变量必须先声明并初始化才能使用,而成员变量有默认值(如int默认 0,String默认null)。

  2. 方法不能嵌套定义(不同于 JS

    java
    class Demo {
        // ❌ 方法不能嵌套定义
        public void fn1() {
            public void fn2() {}
        }
    }

内存流程图:方法调用

示例代码

java
public static void main(String[] args) {
  Person p1 = new Person();
  int sum = p1.getSum(10, 20);
  System.out.println("两数之和:" + sum);
}

class Person {
  public int getSum(int num1, int num2) {
    return num1 + num2;
  }
}

方法调用流程

  1. 执行方法时,JVM 会开辟独立的栈空间(方法栈)。
  2. 方法执行完毕或遇到 return 时,栈空间释放,返回调用处。
  3. 返回后继续执行方法后面的代码。
  4. 当 main 方法(栈)执行完毕后,整个程序退出。

内存流程图

成员方法作用

成员方法作用

  1. 提高代码复用性:避免重复编写相同逻辑。
  2. 封装实现细节:用户无需关心内部逻辑,直接调用。

示例:封装二维数组遍历方法

定义MyTools类的printArr方法,实现二维数组遍历:

java
public class Method02 {
    public static void main(String[] args) {
        int[][] map = {{0,0,1},{1,1,1},{1,1,3}};
        // 2. 多次调用方法,复用代码
        MyTools tool = new MyTools();
        tool.printArr(map);
        tool.printArr(map);
    }
}

class MyTools {
    // 1. 封装方法:遍历二维数组
    public void printArr(int[][] map) {
        for (int i = 0; i < map.length; i++) {
            for (int j = 0; j < map[i].length; j++) {
                System.out.print(map[i][j] + "\t");
            }
            System.out.println();
        }
    }
}

方法调用

方法调用注意事项

  1. 本类中方法调用:直接调用(如sayOk()调用print())。

    java
    class A {
        public void print(int n) { 
            System.out.println("print方法被调用,n=" + n);
        }
    
        public void sayOk() {
            print(10); // 本类中调用:直接调用
        }
    }
  2. 跨类方法调用:通过对象名调用(如B类对象调用A类方法)。

    java
    class A {
        public void m1() {
            // 跨类调用:通过B类对象名调用
            B b = new B();
            b.hi(); 
        }
    }
    
    class B {
        public void hi() { 
            System.out.println("B类的hi方法被执行");
        }
    }
  3. 注意:访问修饰符【后续再说】

    跨类的方法调用和方法的访问呢修饰符也有关系。

练习

  1. 练习 1:判断奇偶方法

    编写AA类的isOdd(int num)方法:判断一个数是奇数还是偶数,返回boolean

    java
    // 定义方法:判断奇偶
    public boolean isOdd(int num) {
      return num % 2 != 0;
    }
    
    // 调用方法
    System.out.println(a.isOdd(5)); // true
    System.out.println(a.isOdd(4)); // false
  2. 练习 2:打印图形方法

    编写AA类的print(int row, int col, char c)方法:根据行、列、字符,打印对应格式的图形。

    java
    // 定义方法:打印图形
    public void print(int row, int col, char c) {
        for (int i = 0; i < row; i++) {
            for (int j = 0; j < col; j++) {
                System.out.print(c);
            }
            System.out.println(); // 换行
        }
    }
    
    // 调用方法
    a.print(4, 4, '#');

方法传参机制@

Java 的方法传参机制是面试高频考点,也是理解方法调用底层逻辑的核心。核心结论:Java 中只有一种传参方式 —— 值传递(Pass by Value),不存在 “引用传递(Pass by Reference)”。很多开发者误以为引用类型是 “引用传递”,本质是对 “值” 的定义理解偏差:引用类型传递的是 “对象引用地址的副本”,而非引用本身,这仍属于值传递范畴。

值传递的本质

值传递的核心

调用方法时,JVM 会创建实参的副本,并将这个副本传递给方法的形参;方法内部对形参的所有修改,仅作用于副本,不会直接影响原始实参。

对比容易混淆的 “引用传递”(Java 不支持)

引用传递是将实参的内存地址直接传递给形参,形参和实参指向同一个内存位置,方法内修改形参会直接改变实参。

基本数据类型

底层原理

基本数据类型(int/double/boolean等)的实参存储在栈内存中,传参时会复制实参的数值本身给形参:

  • 形参是栈中新创建的局部变量,与实参完全独立;

  • 方法内修改形参的数值,仅改变栈中副本,原实参不受影响。

代码示例 + 内存解析

java
public class BasicTypeParam {
    // 方法:修改形参a的值
    public static void changeInt(int a) {
        a = 10; // 仅修改栈中形参副本
        System.out.println("方法内a的值:" + a); // 输出:10
    }

    public static void main(String[] args) {
        int num = 5; // 实参num:栈中存储值5
        changeInt(num); // 传递num的副本(5)给形参a
        System.out.println("方法外num的值:" + num); // 输出:5(原实参未变)
    }
}

内存执行流程(文字图解)

  1. main方法执行时,栈中创建变量num,赋值为5(栈地址:比如0x001,值:5);
  2. 调用changeInt(num)时,JVM 在栈中为形参a分配新空间(栈地址:0x002),将num5复制给a
  3. 方法内执行a=10,仅修改0x002地址的值为100x001地址的num仍为5
  4. 方法执行完毕,形参a出栈销毁,main方法中num的值保持5

引用数据类型

底层原理

引用数据类型(自定义类 / 数组 / 集合 / String 等)的实参存储逻辑:

  • 栈内存:存储对象的引用地址(指向堆内存中的实际对象);

  • 堆内存:存储对象的属性 / 数据。

传参时,JVM 复制的是栈中的引用地址副本给形参:

  • 形参和实参的地址副本指向同一个堆对象

  • 方法内通过地址副本修改堆对象的属性:会影响原对象(因为指向同一个堆内存);

  • 方法内修改形参的地址指向(比如new新对象):仅修改副本的地址,原实参的地址不变,原对象不受影响。

场景 1:修改堆对象的属性(影响原对象)

java
// 自定义引用类型
class Person {
    String name;
    int age;

    public Person(String name, int age) {
        this.name = name;
        this.age = age;
    }
}

public class RefTypeParam1 {
    // 方法:修改对象的age属性
    public static void changePersonAttr(Person p) {
        p.age = 20; // 通过地址副本修改堆对象属性
        System.out.println("方法内p的age:" + p.age); // 输出:20
    }

    public static void main(String[] args) {
        // 实参person:栈存地址0x003,堆存{name:"张三", age:18}
        Person person = new Person("张三", 18);
        changePersonAttr(person); // 传递地址副本0x003给形参p
        System.out.println("方法外person的age:" + person.age); // 输出:20(原对象被修改)
    }
}

内存执行流程

  1. main中创建Person对象:栈中person存地址0x003,堆中0x003地址存储{name:"张三", age:18}
  2. 调用方法时,形参p得到地址副本0x003,此时personp都指向堆中同一个对象;
  3. 方法内p.age=20:直接修改堆中0x003地址的age属性,原personage同步变化;
  4. 方法结束后,p出栈,person仍指向0x003age20

场景 2:修改引用地址的指向(不影响原对象)

java
public class RefTypeParam2 {
    // 方法:修改形参p的地址指向
    public static void changePersonRef(Person p) {
        // 形参p指向新的堆对象(地址0x004)
        p = new Person("李四", 25);
        System.out.println("方法内p的name:" + p.name); // 输出:李四
    }

    public static void main(String[] args) {
        Person person = new Person("张三", 18); // 指向0x003
        changePersonRef(person); // 传递0x003给p
        System.out.println("方法外person的name:" + person.name); // 输出:张三(原对象未变)
    }
}

内存执行流程

  1. mainperson指向堆地址0x003(张三,18);
  2. 方法调用时,p得到0x003的副本,此时pperson都指向0x003
  3. 方法内p = new Person(...)p的地址改为0x004(李四,25),但person仍指向0x003
  4. 方法结束后,p出栈,person的地址和指向的对象均未变化。

不可变类型

StringIntegerDouble等包装类属于不可变类型:对象一旦创建,内部属性(如Stringvalue数组、Integervalue)无法修改,所有 “修改” 操作本质是创建新对象。

String 传参示例

java
public class StringParam {
    public static void changeString(String s) {
        s = "李四"; // 不是修改原对象,而是创建新String对象(地址0x005)
        System.out.println("方法内s的值:" + s); // 输出:李四
    }

    public static void main(String[] args) {
        String str = "张三"; // 栈中str指向常量池地址0x004("张三")
        changeString(str); // 传递0x004给s
        System.out.println("方法外str的值:" + str); // 输出:张三(原对象未变)
    }
}

Integer 包装类传参示例

java
public class IntegerParam {
    public static void changeInteger(Integer i) {
        i = 20; // 自动装箱,创建新Integer对象(地址0x006)
        System.out.println("方法内i的值:" + i); // 输出:20
    }

    public static void main(String[] args) {
        Integer num = 10; // 指向常量池地址0x005(10)
        changeInteger(num);
        System.out.println("方法外num的值:" + num); // 输出:10
    }
}

核心原因

不可变类型的 “修改” 本质是重新赋值,即让形参指向新对象,但原实参的地址仍指向旧对象,因此无法影响原实参。

递归

概述

定义

递归(Recursion):是指方法在执行过程中直接或间接调用自身的编程方式,用于解决满足以下条件的问题:

  • 问题可拆解为规模更小的同类子问题(子问题与原问题逻辑一致);

  • 存在终止条件(当问题规模缩小到一定程度时,无需递归直接返回结果)。

递归能解决的问题

  1. 数学问题:阶乘、斐波那契数列、汉诺塔、八皇后、球和篮子问题。
  2. 算法问题:快排、归并排序、二分查找、分治算法。
  3. 栈相关问题:迷宫路径查找。

本质:分治思想

递归的核心:是 “分而治之”:将大问题拆解为多个小问题,逐个解决小问题后,合并结果得到原问题的解。例如:

  • 计算 n!(n 的阶乘):n! = n * (n-1)!,子问题是 (n-1)!,终止条件是 0! = 1;

  • 遍历二叉树:先递归遍历左子树,再处理当前节点,最后递归遍历右子树,终止条件是 “节点为 null”。

工作原理

Java 中方法调用通过方法栈(栈帧) 实现,递归调用的本质是栈帧的 “压栈” 与 “出栈” 过程:

  1. 压栈(递归调用):每次调用方法时,JVM 会创建新的栈帧(存储方法的参数、局部变量、返回地址),并压入栈顶;

  2. 出栈(递归返回):当方法执行到终止条件或完成逻辑时,栈帧弹出,返回结果给上一层调用者,直到栈为空(回到最初的调用方法)。

示例:阶乘递归的栈帧变化

以 factorial(3) 为例,逐步解析栈的变化:

java
// 阶乘递归方法
public static int factorial(int n) {
    if (n == 0) return 1; // 终止条件
    return n * factorial(n-1); // 递归调用
}

内存流程图与栈帧变化

优缺点

优点

  • 代码简洁:无需手动管理循环逻辑,直接映射问题的数学模型或逻辑结构(如树遍历);

  • 逻辑清晰:符合人类 “分治” 思维,便于理解和维护;

  • 适用场景广:天然适配树、图、分治算法(如归并排序、快速排序)。

缺点

  • 栈溢出风险:递归深度过大时,方法栈会不断压栈,超出 JVM 栈容量(默认约 1MB),抛出 StackOverflowError;

  • 重复计算:如原生斐波那契数列递归,会重复计算大量子问题(如 fib(5) 需计算 fib(4) 和 fib(3),fib(4) 又需计算 fib(3),导致时间复杂度 O (2ⁿ));

  • 效率较低:方法调用有额外的栈帧开销(压栈、出栈、参数传递),比迭代效率低。

注意事项

  1. 必须明确终止条件:且终止条件需能被触发(如 n 逐步递减到 0,而非递增);

  2. 控制递归深度:避免深度过大(如超过 1000 层),优先用迭代或记忆化搜索;

  3. 避免重复计算:复杂递归问题优先考虑缓存(记忆化搜索);

  4. 处理非法输入:如阶乘的 n<0、斐波那契的 n,需抛出异常或返回默认值;

  5. 警惕对象引用共享:递归方法中若使用全局变量或共享对象,需注意线程安全和状态污染(优先用局部变量传递状态)。

核心要素

递归的正确执行依赖两个不可缺少的要素,缺少任何一个都会导致程序异常(死循环、栈溢出):

  • 终止条件:当问题规模缩小到某个临界值时,直接返回结果,不再递归调用。
  • 递归步骤:将原问题拆解为规模更小、逻辑相同的子问题,通过调用自身解决子问题。
终止条件

终止条件:当问题规模缩小到某个临界值时,直接返回结果,不再递归调用;

作用:阻止方法无限调用自身,避免栈溢出(StackOverflowError);

示例:阶乘的 n==0、斐波那契数列的 n==1 || n==2、树遍历的 node==null。

错误示例(无终止条件)

java
// 无终止条件,会无限递归导致栈溢出
public static int badRecursion(int n) {
    return n + badRecursion(n-1);
}
递归步骤

递归步骤:将原问题拆解为规模更小、逻辑相同的子问题,通过调用自身解决子问题;

要求:子问题必须 “逐步逼近” 终止条件(否则仍会无限递归);

示例:n! = n * (n-1)!(子问题 (n-1)! 的规模比 n! 小 1,逐步逼近 n=0)。

常见应用场景

递归适用于 “问题可分治、结构有自相似性” 的场景,以下是 Java 中最典型的应用:

数学问题

打印问题
java
/** 打印问题 */
public void test(int n) { 
    if (n > 2) {
        test(n - 1); // 递归调用
    }
    System.out.println("n=" + n);
}

test(4); // 输出:n=2,n=3,n=4

内存流程图

阶乘计算
java
public class FactorialDemo {
    public static int factorial(int n) {
        // 终止条件:n<0抛异常,n=0返回1
        if (n < 0) throw new IllegalArgumentException("n不能为负数");
        if (n == 0) return 1;
        // 递推关系
        return n * factorial(n - 1);
    }

    public static void main(String[] args) {
        System.out.println(factorial(5)); // 120
    }
}

内存流程图

斐波那契数列
java
/**
 * 斐波那契数列:F(1)=1, F(2)=1, F(n)=F(n-1)+F(n-2)
 * 注意:原生递归存在大量重复计算,下文会优化
 */
public static int fibonacci(int n) {
    if (n <= 0) throw new IllegalArgumentException("n 必须大于 0");
    // 终止条件
    if (n == 1 || n == 2) return 1;
    // 递归步骤
    return fibonacci(n - 1) + fibonacci(n - 2);
}

fibonacci(5) // 1 1 2 3 5

内存流程图

数据结构操作

二叉树的前序遍历
java
class TreeNode {
    int val;
    TreeNode left;
    TreeNode right;
    TreeNode(int val) { this.val = val; }
}

/**
 * 二叉树前序遍历:递归版
 */
public static void preOrderTraversal(TreeNode node) {
    // 终止条件:节点为 null 时返回
    if (node == null) return;
    // 处理当前节点
    System.out.print(node.val + " ");
    // 递归遍历左子树
    preOrderTraversal(node.left);
    // 递归遍历右子树
    preOrderTraversal(node.right);
}

// 调用:preOrderTraversal(root) → 输出根、左子树、右子树节点值
链表反转
java
class ListNode {
    int val;
    ListNode next;
    ListNode(int val) { this.val = val; }
}

/**
 * 链表反转:递归版
 */
public static ListNode reverseList(ListNode head) {
    // 终止条件:空链表或只有一个节点,直接返回自身
    if (head == null || head.next == null) return head;
    // 递归反转后续链表,得到反转后的头节点
    ListNode newHead = reverseList(head.next);
    // 调整当前节点与下一个节点的指向
    head.next.next = head;
    head.next = null;
    // 返回反转后的头节点
    return newHead;
}

文件目录遍历

java
import java.io.File;

/**
 * 递归遍历指定目录下的所有文件(包括子目录)
 */
public static void traverseDirectory(File dir) {
    // 终止条件:不是目录或目录不存在
    if (!dir.exists() || !dir.isDirectory()) return;
    // 获取目录下的所有文件/子目录
    File[] files = dir.listFiles();
    if (files == null) return; // 防止权限问题导致的 null
    // 遍历每个文件/子目录
    for (File file : files) {
        if (file.isDirectory()) {
            // 子目录:递归遍历
            traverseDirectory(file);
        } else {
            // 文件:输出路径
            System.out.println("文件路径:" + file.getAbsolutePath());
        }
    }
}

// 调用:traverseDirectory(new File("D:/test")) → 遍历 D:/test 下所有文件

经典递归问题

猴子吃桃

问题:猴子第一天吃了桃子的一半多 1 个,以后每天吃剩下的一半多 1 个,第 5 天剩 1 个桃子。求最初桃子总数。

思路(逆推)

  • 第 5 天:1 个
  • 第 4 天:(1+1)×2=4 个
  • 第 3 天:(4+1)×2=10 个
  • 规律:前一天桃子数 = (后一天桃子数 + 1)×2
java
public class RecursionExercise01 {
    public int peach(int day) {
        if (day == 5) {
            return 1; // 终止条件(第5天剩1个)
        } else if (day >= 1 && day <= 9) {
            return (peach(day + 1) + 1) * 2; // 递归调用(逆推)
        } else {
            System.out.println("day必须在[1,10]之间");
            return -1;
        }
    }

    public static void main(String[] args) {
        int day = 1;
        int peachNum = peach(day);
        if (peachNum != -1) {
            System.out.println("第" + day + "天有" + peachNum + "个桃子"); // 输出1534
        }
    }
}

内存流程图

迷宫问题

问题:用二维数组表示迷宫,0 表示可走,1 表示障碍物,2 表示已走通,3 表示死路。使用递归回溯找通路。

java
public class MazeRecursion {
    // 1. 构建迷宫:8行7列,1=墙,0=通路
    public static int[][] createMaze() {
        int[][] maze = new int[8][7];
        // 初始化所有位置为0(通路)
        for (int i = 0; i < 8; i++) {
            for (int j = 0; j < 7; j++) {
                maze[i][j] = 0;
            }
        }
        // 构建上下边界(墙)
        for (int j = 0; j < 7; j++) {
            maze[0][j] = 1; // 第一行
            maze[7][j] = 1; // 第八行
        }
        // 构建左右边界(墙)
        for (int i = 0; i < 8; i++) {
            maze[i][0] = 1; // 第一列
            maze[i][6] = 1; // 第七列
        }
        // 构建内部墙
        maze[3][1] = 1;
        maze[3][2] = 1;
        return maze;
    }

    // 2. 打印迷宫:直观展示路径
    public static void printMaze(int[][] maze) {
        for (int i = 0; i < maze.length; i++) {
            for (int j = 0; j < maze[i].length; j++) {
                System.out.print(maze[i][j] + " ");
            }
            System.out.println();
        }
    }

    /**
     * 3. 递归找迷宫路径
     * @param maze 迷宫数组
     * @param i 当前行
     * @param j 当前列
     * @param endI 终点行
     * @param endJ 终点列
     * @return 是否找到通路
     */
    public static boolean findPath(int[][] maze, int i, int j, int endI, int endJ) {
        // 递归终止条件:到达终点
        if (maze[endI][endJ] == 2) {
            return true;
        }

        // 合法性检查:当前位置在范围内,且是未走的通路(0)
        if (i >= 0 && i < maze.length && j >= 0 && j < maze[0].length && maze[i][j] == 0) {
            // 标记当前位置为已走的通路(2)
            maze[i][j] = 2;

            // 按优先级尝试方向:下 → 右 → 上 → 左
            // 1. 尝试向下走
            if (findPath(maze, i + 1, j, endI, endJ)) {
                return true;
            }
            // 2. 向下走不通,尝试向右走
            else if (findPath(maze, i, j + 1, endI, endJ)) {
                return true;
            }
            // 3. 向右走不通,尝试向上走
            else if (findPath(maze, i - 1, j, endI, endJ)) {
                return true;
            }
            // 4. 向上走不通,尝试向左走
            else if (findPath(maze, i, j - 1, endI, endJ)) {
                return true;
            }
            // 5. 所有方向都走不通,标记为死路(3),回溯
            else {
                maze[i][j] = 3;
                return false;
            }
        }

        // 当前位置不合法(墙/死路/越界),返回false
        return false;
    }

    // 主方法测试
    public static void main(String[] args) {
        // 1. 创建迷宫
        int[][] maze = createMaze();
        System.out.println("=== 初始迷宫 ===");
        printMaze(maze);

        // 2. 递归找路径:起点(1,1),终点(6,5)
        boolean hasPath = findPath(maze, 1, 1, 6, 5);

        // 3. 输出结果
        System.out.println("\n=== 路径查找结果 ===");
        if (hasPath) {
            System.out.println("找到通路!迷宫路径如下:");
            printMaze(maze);
        } else {
            System.out.println("未找到通路!");
        }
    }
}

内存流程图

汉诺塔

规则:3 根柱子(A、B、C),n 个圆盘从 A 移到 C,每次只能移 1 个,小圆盘不能放大圆盘下。

java
public class HanoiTower {
    // num:要移动的圆盘数;a:源柱子;b:辅助柱子;c:目标柱子
    public void move(int num, char a, char b, char c) {
        if (num == 1) {
            System.out.println(a + "->" + c); // 1个圆盘直接移
        } else {
            // 如果有多个圆盘,可以看成2个圆盘:最下面的和上面的所有盘
            // 1. 把num-1个圆盘从A移到B,借助C
            move(num - 1, a, c, b);
            // 2. 把最下面的1个圆盘从A移到C
            System.out.println(a + "->" + c);
            // 3. 把num-1个圆盘从B移到C,借助A
            move(num - 1, b, a, c);
        }
    }

    public static void main(String[] args) {
        move(3, 'A', 'B', 'C'); // 3个圆盘,从A移到C,借助B
    }
}

内存流程图

八皇后

问题:在 8×8 棋盘上摆放 8 个皇后,任意两个皇后不能同行、同列、同斜线,求摆法总数。

思路(回溯法)

  1. 用一维数组arr[8]表示,arr[i]表示第 i 行皇后的列号。
  2. 逐行摆放皇后,每放一个皇后,检查是否与已摆放的皇后冲突。
  3. 冲突则回溯,更换列号;无冲突则继续摆下一行,直到 8 个皇后全部摆放完毕(找到一个解)。
数组求和
java
/**
 * 递归求和:数组 arr 从索引 start 到末尾的和
 */
public static int sumArray(int[] arr, int start) {
    // 终止条件:start 超出数组长度,和为 0
    if (start >= arr.length) return 0;
    // 递归步骤:当前元素 + 后续元素的和
    return arr[start] + sumArray(arr, start + 1);
}

sumArray(new int[]{1,2,3,4}, 0) // 调用:→ 10

优化方案

记忆化搜索

针对重复计算问题,用数组、HashMap 等缓存已计算的子问题结果,避免重复计算。

优化斐波那契数列(记忆化搜索)

java
import java.util.HashMap;
import java.util.Map;

public class FibonacciOptimized {
    // 缓存已计算的结果:key = n,value = F(n)
    private static Map cache = new HashMap

    public static int fib(int n) {
        if (n ) throw new IllegalArgumentException("n 必须大于 0");
        // 终止条件
        if (n == 1 || n == 2) return 1;
        // 检查缓存:已计算则直接返回
        if (cache.containsKey(n)) {
            return cache.get(n);
        }
        // 递归计算并缓存结果
        int result = fib(n - 1) + fib(n - 2);
        cache.put(n, result);
        return result;
    }

    // 时间复杂度优化为 O(n),空间复杂度 O(n)(缓存 + 栈)
}

迭代改写

将递归逻辑改为循环,手动管理计算过程,避免栈溢出和方法调用开销。

迭代改写斐波那契数列

java
public static int fibIterative(int n) {
    if (n  throw new IllegalArgumentException("n 必须大于 0");
    if (n == 1 || n == 2) return 1;
    int a = 1, b = 1; // a = F(n-2), b = F(n-1)
    int result = 0;
    for (int i = 3; i ; i++) {
        result = a + b;
        a = b; // 更新 F(n-2) 为原 F(n-1)
        b = result; // 更新 F(n-1) 为当前结果
    }
    return result;
}

// 时间复杂度 O(n),空间复杂度 O(1)(无栈开销,无缓存)

尾递归

尾递归是指递归调用是方法的最后一个操作,无后续计算(如 return fibTail(n-1, a, b))。理论上,编译器可优化尾递归,复用栈帧(无需压栈新帧),避免栈溢出。

Java 编译器不支持尾递归优化(JVM 未实现),即使写尾递归,仍会压栈导致栈溢出。示例如下(仅作理论参考):

java
/**
 * 尾递归版斐波那契数列(Java 不优化,仍可能栈溢出)
 * @param n 目标项
 * @param a F(n-2)
 * @param b F(n-1)
 */
public static int fibTail(int n, int a, int b) {
    if (n == 1) return a;
    if (n == 2) return b;
    // 递归调用是最后一个操作(尾递归)
    return fibTail(n - 1, b, a + b);
}

// 调用:fibTail(5, 1, 1) → 5

增加 JVM 栈容量

通过 JVM 参数 -Xss 增大栈容量(如 -Xss2m 表示栈容量为 2MB),可缓解栈溢出,但不根本解决问题(递归深度过大仍会溢出),不推荐依赖。

方法重载

概述

方法重载(Method Overload):在同一个类中,存在多个方法满足以下条件:

  • 方法名称完全相同
  • 参数列表必须不同(参数个数、参数类型、参数顺序,三者满足其一即可);
  • 与参数名、返回值类型、访问修饰符、抛出异常列表无关

本质:编译时静态绑定

Java 编译器在编译阶段(而非运行阶段),会根据调用方法时传入的实参的静态类型(声明类型),匹配对应的重载方法并确定调用目标,这个过程称为 “静态绑定”。

方法重写(Override):是运行时动态绑定,根据对象的实际类型确定调用方法。

重载的核心价值

  • 语义统一:同一类操作(如 “加法”“打印”)用同一个方法名,符合 “见名知意” 的编码习惯;
  • 简化调用:调用者无需记忆不同功能的方法名,仅需传入不同参数即可;
  • 适配多场景:支持同一逻辑适配不同输入参数(如计算不同个数的数值和、处理不同类型的数据源)。

快速入门

java
public class OverLoad01 {
    // 1. 两个int的和
    public int calculate(int n1, int n2) { 
        System.out.println("调用:两个int的和");
        return n1 + n2;
    }

    // 2. int+double的和
    public double calculate(int n1, double n2) { 
        System.out.println("调用:int+double的和");
        return n1 + n2;
    }

    // 3. double+int的和
    public double calculate(double n1, int n2) { 
        System.out.println("调用:double+int的和");
        return n1 + n2;
    }

    // 4. 三个int的和
    public int calculate(int n1, int n2, int n3) { 
        System.out.println("调用:三个int的和");
        return n1 + n2 + n3;
    }

    public static void main(String[] args) {
        System.out.println(calculate(1, 2)); // 调用两个int参数的方法
        System.out.println(calculate(1, 2.1)); // 调用int+double的方法
        System.out.println(calculate(1.1, 2)); // 调用double+int的方法
        System.out.println(calculate(1, 2, 3)); // 调用三个int参数的方法
    }
}

编译器匹配优先级

编译器在匹配重载方法时,遵循 “精确匹配 → 自动类型转换匹配 → 可变参数匹配” 的优先级,逐级匹配,直到找到唯一匹配的方法;若匹配到多个或无匹配,均会报错。

  • 优先级 1:精确匹配(最优先)

    实参类型与形参类型完全一致,直接匹配:

    java
    public class OverloadMatch {
        public static void print(int a) {
            System.out.println("int类型:" + a);
        }
    
        public static void print(double a) {
            System.out.println("double类型:" + a);
        }
    
        public static void main(String[] args) {
            print(10);    // 精确匹配print(int) → int类型:10
            print(10.5);  // 精确匹配print(double) → double类型:10.5
        }
    }
  • 优先级 2:自动类型转换匹配

    若无精确匹配,编译器会尝试自动向上转型(如 int → long → float → doublechar → int)匹配:

    注意:若存在多个可转换的匹配(如同时有 longfloat 重载),编译器会报错(模糊的方法调用)。

    java
    public class OverloadMatch {
        public static void print(long a) {
            System.out.println("long类型:" + a);
        }
    
        public static void print(double a) {
            System.out.println("double类型:" + a);
        }
    
        public static void main(String[] args) {
            print(10); // int无精确匹配,自动转long → long类型:10
            print('a');// char无精确匹配,自动转int→再转long → long类型:97
        }
    }
  • 优先级 3:可变参数匹配(最低)

仅当精确匹配、自动转换匹配均失败时,才会匹配可变参数(...)的重载方法:

java
public class OverloadMatch {
    public static void print(int... nums) {
        System.out.println("可变参数:" + Arrays.toString(nums));
    }

    public static void main(String[] args) {
        print(10, 20); // 无精确匹配,匹配可变参数 → 可变参数:[10, 20]
    }
}

练习

练习 1:判断题

java
与 `void show(int a, char b, double c)` 构成重载的有(b c d e):
- a) void show(int x, char y, double z){} // 否(形参列表完全相同)
- b) int show(int a, double c, char b){} // 是(形参顺序不同)
- c) void show(int a, double c, char b){} // 是(形参顺序不同)
- d) boolean show(int c, char b){} // 是(形参个数不同)
- e) void show(double c){} // 是(形参个数、类型不同)
- f) double show(int x, char y, double z){} // 否(形参列表完全相同)
- g) void shows(){} // 否(方法名不同)

练习 2:重载方法实现

  1. 定义Methods类的 3 个重载方法m

    • m(int n):输出 n 的平方。
    • m(int n1, int n2):输出 n1×n2 的结果。
    • m(String str):输出字符串。
    java
    class Methods {
        // 1. 定义 m 方法的重载
        public void m(int n) {
            System.out.println("平方=" + (n * n));
        }
    
        public void m(int n1, int n2) {
            System.out.println("相乘=" + (n1 * n2));
        }
    
        public void m(String str) {
            System.out.println("传入的str=" + str);
        }
    }
    
    public class OverLoadExercise {
        public static void main(String[] args) {
            Methods method = new Methods();
            // 2. 测试 m 方法
            method.m(10); // 输出:平方=100
            method.m(10, 20); // 输出:相乘=200
            method.m("韩顺平教育hello"); // 输出:传入的str=韩顺平教育hello
        }
    }
  2. 定义 3 个重载方法max

    • max(int n1, int n2):返回两个 int 的最大值。
    • max(double n1, double n2):返回两个 double 的最大值。
    • max(double n1, double n2, double n3):返回三个 double 的最大值。
    java
    class Methods {
        // 1. 定义 max 方法的重载
        public int max(int n1, int n2) {
            return n1 > n2 ? n1 : n2;
        }
    
        public double max(double n1, double n2) {
            return n1 > n2 ? n1 : n2;
        }
    
        public double max(double n1, double n2, double n3) {
            double max1 = n1 > n2 ? n1 : n2;
            return max1 > n3 ? max1 : n3;
        }
    }
    
    public class OverLoadExercise {
        public static void main(String[] args) {
            Methods method = new Methods();
            // 2. 测试 max 方法
            System.out.println(method.max(10, 24)); // 24
            System.out.println(method.max(10.0, 21.4)); // 21.4
            System.out.println(method.max(10.0, 1.4, 30)); // 30.0
        }
    }

可变参数

概述

可变参数(Variable Arguments,Varargs):是方法参数的一种特殊形式,通过 类型名... 参数名 声明,允许调用方法时传入 0 个、1 个或多个同类型参数,编译器会自动将这些参数封装为一个数组,方法内部可直接将可变参数当作数组处理。

核心本质:数组的 “语法糖”

可变参数并非新的参数类型,而是 JDK 为简化数组参数调用设计的语法糖 —— 编译阶段,编译器会将 类型... 参数名 转换为 类型[] 参数名,方法调用时传入的零散参数会被自动打包为数组。

示例:验证数组本质

编译后,sum(int... nums) 会被转换为 sum(int[] nums),上述调用中,sum(1,2,3) 会被编译器自动转换为 sum(new int[]{1,2,3})

java
public class VarargsDemo {
    // 可变参数方法
    public static int sum(int... nums) {
        int total = 0;
        // 可变参数本质是数组,可遍历
        for (int num : nums) {
            total += num;
        }
        return total;
    }

    public static void main(String[] args) {
        // 调用方式1:传0个参数(数组长度0)
        System.out.println(sum()); // 0
        // 调用方式2:传1个参数
        System.out.println(sum(1)); // 1
        // 调用方式3:传多个参数
        System.out.println(sum(1, 2, 3)); // 6
        // 调用方式4:直接传数组(编译器兼容)
        int[] arr = {4, 5, 6};
        System.out.println(sum(arr)); // 15
    }
}

语法格式

可变参数声明在方法参数列表中,格式为:

java
[修饰符] 返回值类型 方法名([固定参数列表], 类型... 可变参数名) { 
    // 方法体:可变参数当作数组处理
}

快速入门

实现计算任意个数的 int 类型之和。

java
public class VarParameter01 {
    // 可变参数sum:接收任意个数的int,使用时可以当做数组来用
    public int sum(int... nums) { 
        int res = 0;
        for (int i = 0; i < nums.length; i++) {
            res += nums[i];
        }
        return res;
    }

    public static void main(String[] args) {
        System.out.println(sum(1, 5, 100)); // 106(3个数之和)
        System.out.println(sum(1, 19)); // 20(2个数之和)
        System.out.println(sum()); // 0(0个数之和)
    }
}

核心使用规则(必守)

核心使用规则(必守)

  • 规则 1:可变参数必须是参数列表的最后一个

    可变参数接收 “任意数量” 的参数,若后面还有其他参数,编译器无法区分可变参数的结束位置,因此编译报错:

    java
    public class VarargsRule1 {
        // ✅ 正确:可变参数在最后
        public static void print(String prefix, int... nums) {} 
    
        // ❌ 错误:可变参数后有其他参数(编译报错:Varargs parameter must be the last parameter)
        public static void print(int... nums, String suffix) {}
    }
  • 规则 2:一个方法只能有一个可变参数

    同理,多个可变参数会导致编译器无法拆分参数边界,编译报错:

    java
    public class VarargsRule2 {
        // ❌ 错误:一个方法多个可变参数(编译报错:Variable arity parameter must be the last parameter)
        public static void sum(int... nums1, double... nums2) {}
    }
  • 规则 3:可变参数可接收 0 个参数

    调用时不传可变参数,编译器会传入一个空数组(而非 null),方法内部遍历不会报空指针:

    java
    public class VarargsRule3 {
        public static void print(int... nums) {
            System.out.println("数组长度:" + nums.length); // 0个参数时输出0
            for (int num : nums) { // 遍历空数组,无异常
                System.out.println(num);
            }
        }
    
        public static void main(String[] args) {
            print(); // 数组长度:0
        }
    }
  • 规则 4:可变参数支持基本类型和引用类型

    • 基本类型可变参数:int...double... 等;
    • 引用类型可变参数:String...Person... 等。
    java
    class Person {
        private String name;
        public Person(String name) { this.name = name; }
    }
    
    public class VarargsType {
        // 引用类型可变参数
        public static void printPersons(Person... persons) {
            for (Person p : persons) {
                System.out.println(p.name);
            }
        }
    
        public static void main(String[] args) {
            printPersons(new Person("张三"), new Person("李四"));
        }
    }
  • 规则 5:可直接传递数组给可变参数

    编译器兼容数组参数,无需手动拆分,等价于传入多个零散参数:

    java
    public class VarargsArray {
        public static int sum(int... nums) {
            int total = 0;
            for (int num : nums) total += num;
            return total;
        }
    
        public static void main(String[] args) {
            int[] arr = {1,2,3};
            // 直接传数组,等价于sum(1,2,3)
            System.out.println(sum(arr)); // 6
        }
    }

底层实现原理

JVM 本身不支持可变参数,其实现完全依赖编译器的 “语法糖转换”,核心步骤:

  1. 编译方法时:将 类型... 参数名 替换为 类型[] 参数名,方法签名变为 方法名(类型[])
  2. 编译方法调用时
    • 若传入零散参数(如 sum(1,2,3)),自动打包为数组 new int[]{1,2,3}
    • 若传入数组(如 sum(arr)),直接传递数组引用,不做额外处理;
    • 若传入 0 个参数,自动创建空数组 new int[0]

反编译验证(使用 javap -c 命令):

编译 sum(int... nums) 后,字节码中方法签名为 sum([I)I[I 表示 int 数组),与 sum(int[] nums) 完全一致。

练习

封装一个可变参数方法showScore,接收姓名和任意门课成绩,返回“姓名+课程数+总分”。

java
public class VarParameterExercise {
    public String showScore(String name, double... scores) {
        double totalScore = 0;
        for (int i = 0; i < scores.length; i++) {
            totalScore += scores[i];
        }
        return name + " 有" + scores.length + "门课,总分为:" + totalScore;
    }

    public static void main(String[] args) {
        System.out.println(showScore("milan", 90.1, 80.0));
        System.out.println(showScore("terry", 90.1, 80.0, 10, 30.5, 70));
    }
}

作用域

概述

作用域(Scope):是 Java 中程序元素(变量、方法、类、接口等)的可见范围生命周期的集合,核心作用是控制元素的访问权限和内存管理。其中,变量的作用域是最核心、最易混淆的部分(方法 / 类的作用域主要由访问修饰符决定)。

作用域的关键维度

  • 可见性(Visibility):程序的哪个部分能访问该元素(如 “方法内的变量仅方法内可见”)
  • 生命周期(Lifecycle):元素的创建 / 销毁时机(如 “局部变量随方法调用创建,调用结束销毁”)

作用域分类

作用域的核心分类(按范围从小到大)

plaintext
块级作用域 < 方法/构造器作用域 < 类级作用域(成员变量) < 包级作用域 < 全局作用域(public类/方法)

块级作用域

块级作用域(Block Scope):在代码块{} 包裹的区域)内声明的变量,包括:

  • 条件块(if/elseswitch);
  • 循环块(forwhiledo-while);
  • 同步块(synchronized);
  • 普通代码块(直接用 {} 包裹的代码)。

核心特性

  • 生命周期:进入块时在栈内存创建,退出块时立即销毁(栈帧弹出)
  • 可见性:仅在当前块及嵌套的子块内可见,块外无法访问
  • 默认值:无默认值,必须显式初始化后才能使用(否则编译报错)
  • 命名规则:同一块内不能声明同名变量;块嵌套时,内层变量可覆盖外层同名变量(就近原则)

示例

  1. 普通块作用域

    java
    public class BlockScopeDemo {
        public static void main(String[] args) {
            // 外层块
            {
                int a = 10; // 块级变量,作用域:外层块
                System.out.println(a); // 合法:10
    
                // 嵌套块
                {
                    int b = 20; // 作用域:嵌套块
                    System.out.println(a + b); // 合法:30(内层可访问外层)
                    int a = 30; // 合法:内层覆盖外层同名变量(就近原则)
                    System.out.println(a); // 30(访问内层a)
                }
                // System.out.println(b); // 错误:b仅在嵌套块内可见
            }
            // System.out.println(a); // 错误:a仅在外层块内可见
        }
    }
  2. 循环 / 条件块作用域

    java
    public class BlockScopeDemo2 {
        public static void main(String[] args) {
            // for循环块:i的作用域仅在循环内
            for (int i = 0; i < 3; i++) {
                System.out.println(i); // 合法
            }
            // System.out.println(i); // 错误:i超出作用域
    
            // if块:flag的作用域仅在if内
            if (true) {
                boolean flag = true;
                System.out.println(flag); // 合法
            }
            // System.out.println(flag); // 错误:flag超出作用域
        }
    }

方法/构造器作用域

方法 / 构造器作用域(Method/Constructor Scope):在方法 / 构造器内声明的局部变量(包括方法的形参),是块级作用域的 “超集”(方法体本身就是一个大代码块)。

核心特性

  • 生命周期:方法 / 构造器调用时创建(栈帧压入),执行完毕后销毁(栈帧弹出)
  • 可见性:整个方法 / 构造器内可见(包括内部嵌套块),方法外无法访问
  • 默认值:无默认值,必须显式初始化(形参由调用者传值初始化)
  • 命名规则:方法内局部变量不能与形参同名;方法内嵌套块可覆盖方法级变量

示例:方法局部变量 + 形参

java
public class MethodScopeDemo {
    public static int add(int a, int b) { // 1. 方法形参(属于方法作用域)
        int sum = a + b; // 2. 方法级局部变量
        if (sum > 10) {
            int temp = sum * 2; // 嵌套块变量,仅if内可见
            sum = temp;
        }
        // System.out.println(temp); // 错误:temp超出作用域
        return sum;
    }

    public static void main(String[] args) {
        int result = add(3, 5);
        // System.out.println(a); // 错误:形参a超出方法作用域
        // System.out.println(sum); // 错误:sum超出方法作用域
    }
}

类级作用域(成员变量)

类级作用域(成员变量,属性):在类内、方法 / 块外声明的变量(类的成员),分为两类:

  • 实例变量(非静态)
  • 静态变量(类变量)
实例变量

实例变量(Instance Variable)特性

  • 修饰符:无 static 修饰,可加 private/public/protected 等访问修饰符
  • 生命周期:对象创建时在堆内存分配,对象被 GC 回收时销毁
  • 可见性:整个类内可见;外部需通过对象实例访问(obj.var),受访问修饰符限制
  • 默认值:有默认值(同数组规则:int→0,String→null,boolean→false 等)
  • 线程安全:非线程安全(每个对象有独立副本)

示例:实例变量

java
public class ClassScopeDemo {
    // 1. 实例变量(对象级,每个对象独立)
    private String name; 
    private int age; // 默认值0

    public ClassScopeDemo(String name, int age) {
        this.name = name;
        this.age = age;
    }

    public void show() {
        // 2. 类内可直接访问成员变量
        System.out.println("姓名:" + name + ",年龄:" + age);
    }

    public static void main(String[] args) {
        // 3. 类外:必须通过对象访问实例变量
        ClassScopeDemo obj1 = new ClassScopeDemo("张三", 20);
        ClassScopeDemo obj2 = new ClassScopeDemo("李四", 25);
        obj1.show(); // 姓名:张三,年龄:20
        obj2.show(); // 姓名:李四,年龄:25
    }
}
静态变量

静态变量(Class Variable)特性

  • 修饰符static 修饰,可加访问修饰符
  • 生命周期:类加载时在方法区分配,类卸载(JVM 退出)时销毁
  • 可见性:整个类内可见;外部可通过类名 / 对象实例访问(Class.var),受访问修饰符限制
  • 默认值:有默认值(同数组规则:int→0,String→null,boolean→false 等)
  • 线程安全:线程不安全(所有对象共享同一个副本)

示例:静态变量

java
public class ClassScopeDemo {
    // 1. 静态变量(类级,所有对象共享)
    private static String className = "Java基础"; 

    public void show() {
        // 2. 类内可直接访问成员变量(与实例变量相同)
        System.out.println("类名:" + className);
    }

    public static void main(String[] args) {
        // 3. 类外静态变量:通过类名访问(推荐)
        System.out.println(ClassScopeDemo.className); // Java基础

        // 4. 静态变量共享:修改后所有对象可见
        ClassScopeDemo.className = "Java进阶";
        obj1.show(); // 类名:Java进阶
        obj2.show(); // 类名:Java进阶
    }
}

异常参数作用域

异常参数作用域(Catch Scope)catch 块中声明的异常参数(如 catch (Exception e)),属于特殊的块级作用域。

核心特性

  • 生命周期catch 块执行时创建,执行完毕后销毁
  • 可见性:仅在当前 catch 块内可见
  • 命名规则:同一 try-catch 的多个 catch 块可声明同名异常参数(不同作用域)

示例:catch 块异常参数

java
public class CatchScopeDemo {
    public static void main(String[] args) {
        try {
            int a = 1 / 0;
        } catch (ArithmeticException e) {
            // e仅在当前catch块内可见
            System.out.println("算术异常:" + e.getMessage());
        } catch (Exception e) {
            // 不同catch块,同名e合法
            System.out.println("通用异常:" + e.getMessage());
        }
        // System.out.println(e); // 错误:e超出catch作用域
    }
}

内部类作用域

内部类的作用域依赖其定义位置,分为:

  • 成员内部类
  • 局部内部类
  • 匿名内部类
成员内部类

成员内部类类内、方法外的内部类;

可见性:可访问外部类的所有成员(包括 private);

外部类访问内部类:需通过内部类实例(Outer.Inner inner = new Outer().new Inner())。

示例:成员内部类

java
public class InnerClassScopeDemo {
    private String outerVar = "外部类变量";

    // 1. 成员内部类
    class MemberInner {
        public void show() {
            // 可访问外部类private成员
            System.out.println(outerVar); // 外部类变量
        }
    }

    public static void main(String[] args) {
        InnerClassScopeDemo outer = new InnerClassScopeDemo();
        // 2. 成员内部类:外部需通过实例访问
        MemberInner memberInner = outer.new MemberInner();
        memberInner.show();
    }
}
局部内部类

局部内部类方法 / 块内的内部类;

可见性:仅在方法 / 块内可见;

访问规则:可访问外部类的成员,以及外部方法的 final/effectively final 局部变量(JDK 8+)。

示例:局部内部类

java
public class InnerClassScopeDemo {
    private String outerVar = "外部类变量";

    // 方法内局部内部类
    public void testLocalInner() {
        // effectively final变量(未重新赋值)
        String localVar = "方法局部变量";
        // 1. 局部内部类
        class LocalInner {
            public void show() {
                System.out.println(outerVar); // 访问外部类成员
                System.out.println(localVar); // 访问外部方法final变量
            }
        }

        // 2. 局部内部类仅方法内可见
        LocalInner inner = new LocalInner();
        inner.show();
    }

    public static void main(String[] args) {
        InnerClassScopeDemo outer = new InnerClassScopeDemo();
        outer.testLocalInner();
    }
}
匿名内部类

匿名内部类:无类名的局部内部类;

作用域:与局部内部类一致,仅在定义的方法 / 块内有效;

访问规则:同局部内部类。

作用域规则总结

作用域规则总结

  1. 默认值

    • 块级/方法/构造器:没有默认值,必须显式初始化后才能使用(否则编译报错)
    • 类级(实例/静态):有默认值,规则与数组一致(int→0,String→null,boolean→false 等)
  2. 变量重名

    • 同一块内:不能声明同名变量;

      java
      public void hi() {
          String address = "北京";
          // String address = "上海"; // ❌ 错误(同一作用域局部变量重名)
      }
    • 块嵌套时:内层变量可覆盖外层同名变量(就近原则)

      java
      class Person {
          String name = "jack";
      
          public void say() {
              String name = "king"; // 局部变量与属性重名
              System.out.println("say() name=" + name); // 就近原则,输出king
          }
      }
  3. 生命周期

    • 类级(实例/静态):伴随对象创建而创建,伴随对象销毁而销毁。
    • 块级/方法/构造器:伴随代码块执行而创建,伴随代码块结束而销毁。
    java
    class Person {
        // 1. 类级变量 name 是随着 Person 对象的创建而创建,随着 Person 对象的销毁而销毁(持续时间更久)
        String name = "jack";
    
        public void say() {
            // 2. 局部变量 name 会随着 say() 调用而创建,也会随着 say() 调用结束而销毁
            String name = "king";
        }
    }
  4. 访问范围

    • 类级(实例/静态):整个类内可见;外部需通过类名 / 对象实例访问(Class.var / obj.var),受访问修饰符限制。
    • 块级/方法/构造器:整个块 / 方法 / 构造器内可见(包括内部嵌套块),块 / 方法外无法访问。
    java
    class Person {
        String name = "jack";
    
        public void say() {
            String age = "king";
        }
    
        public void hi() {
            System.out.println(age); // ❌ 2. 访问失败
        }
    }
    
    class T {
        public void test() {
            Person p1 = new Person();
            System.out.println(p1.name); // ✅ 1. 其他类通过对象访问属性,输出jack
        }
    }
  5. 修饰符

    • 类级(实例/静态):可加 private/public/protected 等访问修饰符。
    • 块级/方法/构造器:不能加访问修饰符。
    java
    class Person {
        // 1. 属性可以添加修饰符
        public String name = "jack";
    
        public void say() {
            // 2. 局部变量不可以添加修饰符
            // public String age = "king";
        }
    }

构造方法

概述

构造方法(Constructor,构造器):是类中满足以下特征的特殊方法:

  • 方法名必须与类名完全一致(包括大小写,如 Person 类的构造方法名必须是 Person);
  • 没有返回值类型(连 void 都不能声明);
  • 不能被 staticfinalabstractnativesynchronized 等修饰(可被 public/private/protected 访问修饰符修饰);
  • 创建对象时由 JVM 自动调用,而非手动调用(仅能通过 this()/super() 在构造方法内部调用其他构造)。

本质和作用

构造方法的核心价值是保证对象创建时的初始化完整性

  • 初始化对象的成员变量(避免属性处于 “未初始化” 的默认值状态,如 int 默认 0、String 默认 null);
  • 执行对象创建时的必要逻辑(如连接数据库、初始化集合、校验参数合法性);
  • 控制对象的创建方式(如私有构造方法实现单例模式,禁止外部创建对象)。

语法格式

java
[访问修饰符] 类名([参数列表]) [throws 异常类型列表] {
    // 构造方法体:初始化属性、执行初始化逻辑
}

语法注意事项

  1. 无返回值:构造方法不能声明返回值类型(包括 void),以下写法是错误的:

    java
    // 错误:不能写void
    public void Person() {}
    // 错误:不能写返回值类型
    public int Person() { return 1; }
  2. 方法名必须与类名一致:大小写错误会被识别为普通方法,而非构造方法:

    java
    public class Person {
        // 错误:方法名是person(小写),类名是Person(大写),这是普通方法
        public person() {}
    }

快速入门

java
// 定义Person类
public class Person {
    // 成员变量
    private String name;
    private int age;

    // 无参构造方法(自定义)
    public Person() {
        // 初始化默认值
        this.name = "未知";
        this.age = 0;
        System.out.println("无参构造方法被调用");
    }

    // 有参构造方法(自定义)
    public Person(String name, int age) {
        // 初始化传入的属性值
        this.name = name;
        this.age = age;
        System.out.println("有参构造方法被调用");
    }

    // 普通方法(对比构造方法)
    public void showInfo() {
        System.out.println("姓名:" + name + ",年龄:" + age);
    }

    public static void main(String[] args) {
        // 创建对象时自动调用对应构造方法
        Person p1 = new Person(); // 调用无参构造 → 无参构造方法被调用
        p1.showInfo(); // 输出:姓名:未知,年龄:0

        Person p2 = new Person("张三", 20); // 调用有参构造 → 有参构造方法被调用
        p2.showInfo(); // 输出:姓名:张三,年龄:20
    }
}

核心特性

构造方法的核心特性

  1. 自动调用:仅在 new 对象时触发

    构造方法不能像普通方法一样通过 对象名.方法名() 调用,只能在创建对象时由 JVM 自动执行:

    java
    public class Test {
        public static void main(String[] args) {
            Person p = new Person(); // 自动调用构造方法
            // p.Person(); // 错误:构造方法不能手动调用
        }
    }
  2. 默认构造方法(隐式无参构造)

    如果类中没有定义任何构造方法,JVM 会自动生成一个隐式的无参构造方法(默认构造):

    • 访问修饰符与类的修饰符一致(类是 public,默认构造也是 public;类是 default,默认构造也是 default);

    • 方法体为空,仅完成对象的默认初始化(成员变量赋默认值)。

    示例

    java
    public class Person {
        private String name;
        private int age;
    
        // 未定义任何构造方法,JVM自动生成默认无参构造
        // 等价于:public Person() {}
    
        public static void main(String[] args) {
            Person p = new Person(); // 调用默认无参构造
            System.out.println(p.name); // null(默认值)
            System.out.println(p.age);  // 0(默认值)
        }
    }
  3. 默认构造的 “消失规则”

    如果类中自定义了任意构造方法(无论有参 / 无参),JVM 不再自动生成默认无参构造:

    java
    public class Person {
        private String name;
        private int age;
    
        // 自定义有参构造,默认无参构造消失
        public Person(String name) {
            this.name = name;
        }
    
        public static void main(String[] args) {
            // Person p = new Person(); // 错误:找不到无参构造方法
            Person p = new Person("张三"); // 正确:调用自定义有参构造
        }
    }

    解决方案:若需要无参构造,需手动显式定义。

  4. 构造方法可重载(核心特性)

    构造方法支持重载(与普通方法重载规则一致):同一个类中,多个构造方法名相同(类名),参数列表不同(个数 / 类型 / 顺序)

    重载的目的是提供多种对象初始化方式(如无参初始化默认值、有参初始化指定值):

    java
    public class Person {
        private String name;
        private int age;
        private String gender;
    
        // 重载1:无参构造(初始化默认值)
        public Person() {
            this.name = "未知";
            this.age = 0;
            this.gender = "未知";
        }
    
        // 重载2:单参数构造(仅初始化姓名)
        public Person(String name) {
            this.name = name;
            this.age = 0;
            this.gender = "未知";
        }
    
        // 重载3:三参数构造(初始化所有属性)
        public Person(String name, int age, String gender) {
            this.name = name;
            this.age = age;
            this.gender = gender;
        }
    
        public static void main(String[] args) {
            Person p1 = new Person(); // 调用无参构造
            Person p2 = new Person("李四"); // 调用单参数构造
            Person p3 = new Person("王五", 25, "男"); // 调用三参数构造
        }
    }
  5. 构造方法不能被继承

    子类不会继承父类的构造方法,只能通过 super() 调用父类构造方法。

  6. 构造方法不能被 static 修饰

    static 修饰的方法属于类,而构造方法是创建对象时调用的,依赖对象实例,因此冲突:

    java
    // 错误:构造方法不能被static修饰
    public static Person() {}

调用规则

构造方法内部可通过 this() 调用本类其他构造方法,或通过 super() 调用父类构造方法,核心规则:

  1. this()/super() 必须是构造方法体的第一条语句
  2. 不能同时在一个构造方法中调用 this()super()(因为第一条语句只能有一个);
  3. this() 用于重载构造之间的复用,super() 用于初始化父类属性。

this

this ():调用本类其他构造方法

目的是复用构造方法的初始化逻辑,减少代码冗余:

java
public class Person {
    private String name;
    private int age;

    // 无参构造:调用有参构造,传入默认值
    public Person() {
        this("未知", 0); // 调用本类的Person(String, int)构造,必须在第一行
        System.out.println("无参构造执行");
    }

    // 有参构造:核心初始化逻辑
    public Person(String name, int age) {
        this.name = name;
        this.age = age;
        System.out.println("有参构造执行");
    }

    public static void main(String[] args) {
        Person p = new Person();
        // 输出顺序:
        // 有参构造执行
        // 无参构造执行
    }
}

super

super ():调用父类构造方法

子类构造方法必须调用父类构造方法(显式 / 隐式),确保父类属性先初始化:

  • 隐式调用:若子类构造方法中未写 super(),JVM 会自动在第一行插入 super()(调用父类无参构造);

    java
    // 父类
    class Parent {
        private String parentName;
    
        // 父类无参构造
        public Parent() {
            this.parentName = "父类默认名称";
            System.out.println("父类无参构造执行");
        }
    }
    
    // 子类
    class Child extends Parent {
        private String childName;
    
        // 子类无参构造:隐式调用super()(父类无参构造)
        public Child() {
            // 隐式super(),等价于:super();
            this.childName = "子类默认名称";
            System.out.println("子类无参构造执行");
        }
    }
    
    // 测试
    public class Test {
        public static void main(String[] args) {
            Child c1 = new Child();
            // 输出:
            // 父类无参构造执行
            // 子类无参构造执行
        }
    }
  • 显式调用:手动指定 super(参数) 调用父类有参构造,必须在构造方法第一行。

    java
    // 父类
    class Parent {
        private String parentName;
    
        // 父类有参构造
        public Parent(String parentName) {
            this.parentName = parentName;
            System.out.println("父类有参构造执行");
        }
    }
    
    // 子类
    class Child extends Parent {
        private String childName;
    
        // 子类有参构造:显式调用父类有参构造
        public Child(String parentName, String childName) {
            super(parentName); // 调用父类有参构造,必须在第一行
            this.childName = childName;
            System.out.println("子类有参构造执行");
        }
    }
    
    // 测试
    public class Test {
        public static void main(String[] args) {
            Child c2 = new Child("父类自定义名称", "子类自定义名称");
            // 输出:
            // 父类有参构造执行
            // 子类有参构造执行
        }
    }
  • 调用规则注意点:若父类没有无参构造(仅自定义有参构造),子类构造必须显式调用父类有参构造,否则编译报错:

    java
    // 父类:仅自定义有参构造,无默认无参构造
    class Parent {
        public Parent(String name) {}
    }
    
    // 子类:编译错误,因为隐式super()会调用父类无参构造(不存在)
    class Child extends Parent {
        public Child() {
        // 错误:Implicit super constructor Parent() is undefined. Must explicitly invoke another constructor
        }
    
        // 正确:显式调用父类有参构造
        public Child(String name) {
            super(name);
        }
    }

对象初始化顺序

创建对象时,初始化顺序为

静态变量/静态代码块(类加载时执行,仅一次)实例变量/构造代码块(每次创建对象执行)构造方法(每次创建对象执行)

核心结论

  1. 静态相关(变量 / 代码块)在类加载时执行,仅执行一次;

  2. 实例相关(变量 / 构造代码块 / 构造方法)在每次创建对象时执行;

  3. 构造代码块先于构造方法执行(构造代码块是 “所有构造方法的公共逻辑”)。

java
public class InitOrder {
    // 静态变量
    private static String staticVar = "静态变量初始化";

    // 实例变量
    private String instanceVar = "实例变量初始化";

    // 1. 静态代码块:类加载时执行,仅执行一次
    static {
        System.out.println(staticVar);
        System.out.println("静态代码块执行");
    }

    // 2. 构造代码块:每次创建对象时执行,先于构造方法执行
    {
        System.out.println(instanceVar);
        System.out.println("构造代码块执行");
    }

    // 3. 构造方法:每次创建对象时执行
    public InitOrder() {
        System.out.println("构造方法执行");
    }

    public static void main(String[] args) {
        System.out.println("=====创建第一个对象=====");
        InitOrder obj1 = new InitOrder();

        System.out.println("=====创建第二个对象=====");
        InitOrder obj2 = new InitOrder();
    }
}
sh
输出结果:

静态变量初始化
静态代码块执行
=====创建第一个对象=====
实例变量初始化
构造代码块执行
构造方法执行
=====创建第二个对象=====
实例变量初始化
构造代码块执行
构造方法执行

练习

Person类添加两个构造器:

  1. 无参构造器:设置age初始值为 18。
  2. pNamepAge参数的构造器:初始化nameage
java
public class ConstructorExercise {
    public static void main(String[] args) {
        Person p1 = new Person(); // 调用无参构造器
        System.out.println("p1 的信息name=" + p1.name + " age=" + p1.age); // name=null,age=18
        Person p2 = new Person("scott", 50); // 调用带参构造器
        System.out.println("p2 的信息name=" + p2.name + " age=" + p2.age); // name=scott,age=50
    }
}

class Person {
    String name; // 默认值null
    int age; // 默认值0

    // 无参构造器:age初始值18
    public Person() {
        age = 18;
    }

    //  带参构造器:初始化name和age
    public Person(String pName, int pAge) {
        name = pName;
        age = pAge;
    }
}

对象创建内存流程

对象创建内存流程

  1. 加载 Person 类信息(Person.class),只会加载一次
  2. 在堆中分配空间(地址)
  3. 完成对象初始化
    • 3.1 默认初始化:age=0,name=null
    • 3.2 显式初始化:age=90,name=null
    • 3.3 构造器的初始化:age=20,name=小倩
  4. 将对象在堆中的地址,返回给 p(p 是对象名,也可以理解成是对象的引用)

this

概述

this:是 Java 保留关键字,有两种核心语义:

  • 实例上下文:在实例方法 / 构造方法中,this 指代调用当前方法的对象实例(或正在初始化的对象实例),可通过 this 访问对象的成员变量 / 方法;
  • 构造调用:在构造方法中,this(参数) 用于调用本类的其他构造方法,实现构造逻辑复用。

本质:实例方法的隐式参数

Java 中所有实例方法(非 static 方法)都会隐式接收一个 this 参数,该参数由 JVM 在调用方法时自动传递,指向调用此方法的对象实例。

示例:字节码编译验证

从字节码层面看,实例方法的第一个参数永远是 this(类型为当前类),比如:

java
// 源码
public class Person {
    private String name;
    public void setName(String name) {
        this.name = name;  
    }
}

// 编译后的字节码(简化):setName方法的第一个参数是this
// void setName(LPerson; Ljava/lang/String;)V

JVM 调用 person.setName("张三") 时,会将 person 对象的引用作为 this 参数传递给 setName 方法,因此方法内可通过 this 访问该对象的 name 成员。

image-20260105162935290

使用场景

this 的核心使用场景

  1. 场景 1:区分同名的局部变量(形参)与成员变量(属性)

    这是 this 最常用的场景:当方法的局部变量与类的成员变量同名时,this.成员变量 明确指向成员变量,避免 “变量遮蔽”。

    java
    public class Person {
        private String name;
        private int age;
    
        // 形参name/age与成员变量同名,用this区分
        public Person(String name, int age) {
            // this.name:成员变量;name:形参
            this.name = name;
            this.age = age;
        }
    }
  2. 场景 2:调用本类的实例方法

    this.方法名() 可显式调用本类的其他实例方法,虽然省略 this 也能调用,但显式使用可增强代码可读性(尤其是区分继承的方法时)。

    java
    public class Calculator {
        private int result;
    
        public void add(int num) {
            this.result += num;
        }
    
        public void multiply(int num) {
            // 显式调用本类的add方法(省略this也可,但this增强语义)
            this.add(num);
            this.result *= num;
        }
    }
  3. 场景 3:调用本类的其他构造方法

    构造方法中通过 this(参数) 调用本类的重载构造方法,实现构造逻辑复用核心规则

    • this() 必须是构造方法的第一条语句
    • this()参数列表需匹配本类的某个构造方法;
    • 不能递归调用(如构造 A 调 this(),而 this() 又调回构造 A);
    • 不能与 super() 同时使用(二者都需是第一条语句)。
    • this() 语法只能在构造器中使用,不能在其他成员方法中使用。
    java
    public class User {
        private String username;
        private String password;
        private String email;
    
        // 两参构造:调用三参构造,email传默认值
        public User(String username, String password) {
            this(username, password, "default@xxx.com"); // 必须在第一行
            System.out.println("两参构造执行");
        }
    
        // 核心三参构造:初始化所有成员
        public User(String username, String password, String email) {
            this.username = username;
            this.password = password;
            this.email = email;
            System.out.println("三参构造执行");
        }
    
        public static void main(String[] args) {
            User u2 = new User("李四", "654321");
            // 输出顺序:
            // 三参构造执行 → 两参构造执行
        }
    }
  4. 场景 4:返回当前对象(链式调用)

    在实例方法中返回 this(当前对象引用),可实现 “链式调用”(如 Builder 模式、流式编程),简化代码调用。

    java
    public class StringBuilderDemo {
        private StringBuilder sb = new StringBuilder();
    
        // 返回this,支持链式调用
        public StringBuilderDemo append(String str) {
            sb.append(str);
            return this; // 返回当前对象
        }
    
        public StringBuilderDemo append(int num) {
            sb.append(num);
            return this;
        }
    
        public String getResult() {
            return sb.toString();
        }
    
        public static void main(String[] args) {
            // 链式调用:连续调用append方法
            String result = new StringBuilderDemo()
                    .append("Hello")
                    .append("Java")
                    .append(2025)
                    .getResult();
            System.out.println(result); // HelloJava2025
        }
    }
  5. 场景 5:将当前对象作为参数传递

    this 可作为实参传递给其他方法,将当前对象引用传递出去,适用于 “对象协作” 场景(如回调、依赖注入)。

    java
    // 工具类:接收Person对象并打印信息
    class PersonUtil {
        public static void printPerson(Person person) {
            System.out.println("姓名:" + person.getName() + ",年龄:" + person.getAge());
        }
    }
    
    public class Person {
        private String name;
        private int age;
    
        public Person(String name, int age) {
            this.name = name;
            this.age = age;
        }
    
        public void showInfo() {
            // 将当前对象(this)传递给PersonUtil的printPerson方法
            PersonUtil.printPerson(this);
        }
    
        // getter
        public String getName() { return this.name; }
        public int getAge() { return this.age; }
    
        public static void main(String[] args) {
            Person p = new Person("王五", 25);
            p.showInfo(); // 输出:姓名:王五,年龄:25
        }
    }
  6. 场景 6:内部类中访问外部类的 this

    非静态内部类(成员内部类 / 局部内部类)有自己的 this(指向内部类实例),若需访问外部类的 this,需通过 外部类名.this 显式指定。

    java
    public class Outer {
        private String outerVar = "外部类变量";
    
        // 成员内部类
        class Inner {
            private String innerVar = "内部类变量";
            private String outerVar = "内部类覆盖的outerVar";
    
            public void show() {
                // 1. this:指向内部类实例
                System.out.println(this.innerVar); // 内部类变量
                System.out.println(this.outerVar); // 内部类覆盖的outerVar
    
                // 2. Outer.this:指向外部类实例
                System.out.println(Outer.this.outerVar); // 外部类变量
            }
        }
    
        public static void main(String[] args) {
            Outer outer = new Outer();
            Outer.Inner inner = outer.new Inner();
            inner.show();
        }
    }

核心规则

this 的核心规则(必守)

  1. 规则 1:不能在静态上下文使用 this

    静态上下文(静态方法、静态代码块、静态内部类)属于类级别,无对象实例,因此 this 无指向,编译直接报错。

    java
    public class ThisStaticError {
        private static String staticVar = "静态变量";
        private String instanceVar = "实例变量";
    
        // 1. 静态方法:不能使用this
        public static void staticMethod() {
            // System.out.println(this.instanceVar); // 错误:Cannot use this in a static context
            // System.out.println(this); // 错误:同上
        }
    
        // 2. 静态代码块:不能使用this
        static {
            // System.out.println(this.staticVar); // 错误:同上
        }
    
        // 3. 静态内部类:不能访问外部类的this
        static class StaticInner {
            public void show() {
                // System.out.println(ThisStaticError.this.instanceVar); // 错误:静态内部类无外部this
            }
        }
    }
  2. 规则 2:this () 调用构造方法的限制

    • this() 必须是构造方法的第一条语句,否则编译报错;
    • this() 不能递归调用(如 public Person() { this(); } 会无限递归,编译报错);
    • this() 的参数列表必须匹配本类的某个构造方法(参数个数 / 类型 / 顺序一致);
    • 不能同时使用 this()super()(二者都需是第一条语句,冲突)。
    java
    public class ThisConstructorRule {
        public ThisConstructorRule() {
            // System.out.println("非第一条语句"); // 错误:this()必须是第一条
            this(10);
        }
    
        public ThisConstructorRule(int num) {
            // this(); // 错误:递归调用构造方法
        }
    
        // public ThisConstructorRule(String str) {
        //     super(); // 错误:不能同时用super()和this()
        //     this(10);
        // }
    }
  3. 规则 3:this 指向调用方法的对象实例

    同一个类的不同对象调用同一实例方法,this 指向不同的实例,保证每个对象的成员独立。可以通过 .hashCode() 判断 this 指向不同的内存地址。

    java
    public class ThisPointDemo {
        private int num;
    
        public void showThis() {
            System.out.println("this的引用地址:" + this.hashCode());
        }
    
        public static void main(String[] args) {
            ThisPointDemo obj1 = new ThisPointDemo();
            ThisPointDemo obj2 = new ThisPointDemo();
    
            obj1.showThis(); // this指向obj1(地址不同)
            obj2.showThis(); // this指向obj2(地址不同)
        }
    }
  4. 规则 4:this 不能被赋值

    this 是关键字,不是可赋值的变量,试图给 this 赋值会编译报错:

    java
    public class ThisAssignError {
        public void assignThis() {
            // 错误:The left-hand side of an assignment must be a variable
            // this = new ThisAssignError();
        }
    }
  5. 规则 5:this 永远不为 null(合法调用下)

    实例方法必须通过对象调用(obj.method()),JVM 会将 obj 作为 this 传递,因此合法调用下 this 不可能为 null

    例外:通过反射非法调用实例方法时,可传递 null 作为 this,此时方法内使用 this 会抛出 NullPointerException

    java
    import java.lang.reflect.Method;
    
    public class ThisNullReflect {
        public void show() {
            System.out.println(this); // 反射传null时,this为null → NPE
        }
    
        public static void main(String[] args) throws Exception {
            Method method = ThisNullReflect.class.getMethod("show");
            method.invoke(null); // 传递null作为this → 运行时NPE
        }
    }

练习

需求:定义 Person 类,提供compareTo()方法,判断两个 Person 对象的名字和年龄是否完全一致。

java
public class TestPerson {
    public static void main(String[] args) {
        Person p1 = new Person("mary", 20);
        Person p2 = new Person("mary", 20);
        System.out.println("p1 和 p2 比较的结果:" + p1.compareTo(p2)); // true
    }
}

class Person {
    String name;
    int age;

    // 构造器
    public Person(String name, int age) {
        this.name = name;
        this.age = age;
    }

    // 比较方法
    public boolean compareTo(Person p) {
        // this 代表当前对象(调用compareTo方法的对象)
        return this.name.equals(p.name) && this.age == p.age;
    }
}

本章作业

  1. 编写类 A01,定义方法max(),实现求某个 double 数组的最大值,并返回(Homework01.java)
  2. 编写类 A02,定义方法find(),实现查找某字符串是否在字符串数组中,并返回索引;找不到返回-1(Homework02.java)
  3. 编写类 Book,定义方法updatePrice(),更改书的价格:
    • 价格 >150 → 150
    • 价格 >100 → 100
    • 否则不变(Homework03.java)
  4. 编写类 A03,实现数组复制功能copyArr(),输入旧数组,返回新数组(元素和旧数组一致)(Homework04.java)
  5. 定义圆类 Circle,属性:半径;提供方法:显示圆周长、显示圆面积(Homework05.java)
  6. 创建 Calc 计算类,定义两个操作数,提供加减乘除方法(除数为 0 时提示),创建对象测试(Homework06.java)
  7. 设计 Dog 类,属性:名字、颜色、年龄;定义show()方法显示信息(使用 this.属性)(Homework07.java)
  8. 以下代码编译运行后,输出结果是(10,9,10
    java
    public class Test {
        int count = 9;
        public void count1() {
            count = 10;
            System.out.println("count1=" + count);
        }
        public void count2() {
            System.out.println("count1=" + count++);
        }
        public static void main(String args[]) {
            new Test().count1(); // count1=10
            Test t1 = new Test();
            t1.count2(); // count1=9(后置++,先输出后自增)
            t1.count2(); // count1=10
        }
    }
  9. 定义 Music 类,属性:音乐名 name、时长 times;方法:播放 play()、返回属性信息 getInfo()(Homework09.java)
  10. 以下代码运行结果是(101,100,101,101
    java
    class Demo {
        int i = 100;
        public void m() {
            int j = i++; // j=100,i=101
            System.out.println("i=" + i); // 101
            System.out.println("j=" + j); // 100
        }
    }
    class Test {
        public static void main(String[] args) {
            Demo d1 = new Demo();
            Demo d2 = d1; // 引用传递,指向同一个对象
            d2.m();
            System.out.println(d1.i); // 101
            System.out.println(d2.i); // 101
        }
    }
  11. 调用语句System.out.println(method(method(10.0,20.0),100));编译正确,method方法的定义形式为:
    java
    public double method(double d1, double d2) { ... }
  12. 创建 Employee 类,属性:名字、性别、年龄、职位、薪水;提供 3 个构造器(复用构造器):
    • 构造器 1:初始化所有属性
    • 构造器 2:初始化名字、性别、年龄
    • 构造器 3:初始化职位、薪水(Homework12.java)
  13. 将对象作为参数传递给方法(Homework13.java)
    • 定义 Circle 类:属性 radius,方法findArea()返回面积
    • 定义 PassObject 类:方法printAreas(Circle c, int times),打印 1~times 的半径及对应面积
    • 测试类中调用printAreas(),输出结果如下:
      Radius 1.0 → Area 3.141592653589793
      Radius 2.0 → Area 12.566370614359172
      Radius 3.0 → Area 28.274333882308138
      Radius 4.0 → Area 50.26548245743669
      Radius 5.0 → Area 78.53981633974483
  14. 扩展题:设计 Tom 类,实现与电脑猜拳功能(石头 0、剪刀 1、布 2),记录输赢次数(Homework14.java)